package com.j256.ormlite.hmos;

import com.j256.ormlite.db.DatabaseType;
import com.j256.ormlite.db.SqlLiteHarmonyDatabaseType;
import com.j256.ormlite.field.DatabaseField;
import com.j256.ormlite.field.DatabaseFieldConfig;
import com.j256.ormlite.field.ForeignCollectionField;
import com.j256.ormlite.table.DatabaseTable;
import com.j256.ormlite.table.DatabaseTableConfig;
import com.j256.ormlite.table.DatabaseTableConfigLoader;
import com.j256.ormlite.utils.Constants;

import java.io.*;
import java.lang.reflect.Field;
import java.sql.SQLException;
import java.text.SimpleDateFormat;
import java.util.*;

/**
 * OrmLiteConfigUtil
 *
 */
public class OrmLiteConfigUtil {
    /**
     * Resource directory name that we are looking for.
     */
    protected static final String RESOURCE_DIR_NAME = "resources";

    /**
     * Raw directory name that we are looking for.
     */
    protected static final String RAW_DIR_NAME = "raw";

    /**
     * Maximum recursion level while we are looking for source files.
     */
    protected static final int MAXFINDSOURCELEVEL = 20;
    private static final DatabaseType DATABASETYPE = new SqlLiteHarmonyDatabaseType();
    private static final ClassComparator CLASS_COMPARATOR = new ClassComparator();

    /**
     * A call through to {@link #writeConfigFile(File, boolean)}. It takes an optional "-s" argument which turns on
     * sorting of classes by name to produce more deterministic output followed by the name of the output config file.
     *
     * @param args argument
     * @throws Exception exception
     */
    public static void main(String[] args) throws Exception {
        int argCount = 0;
        boolean issortClasses = false;
        for (; argCount < args.length; argCount++) {
            String arg = args[argCount];
            if (arg.equals(Constants.SS)) {
                issortClasses = true;
            } else {
                break;
            }
        }
        // we should have one arg left
        if (argCount != args.length - 1) {
            throw new IllegalArgumentException("Usage: OrmLiteConfigUtil [-s] config-file-name");
        }
        writeConfigFile(args[argCount], issortClasses);
    }

    /**
     * Finds the annotated classes in the current directory or below and writes a configuration file to the file-name in
     * the raw folder.
     *
     * @param fileName name
     * @throws SQLException exception
     * @throws IOException  exception
     */
    public static void writeConfigFile(String fileName) throws SQLException, IOException {
        writeConfigFile(fileName, false);
    }

    /**
     * Finds the annotated classes in the current directory or below and writes a configuration file to the file-name in
     * the raw folder.
     *
     * @param sortClasses Set to true to sort the classes by name before the file is generated.
     * @param fileName    file name
     * @throws SQLException exception
     * @throws IOException  exception
     */
    public static void writeConfigFile(String fileName, boolean sortClasses) throws SQLException, IOException {
        List<Class<?>> classList = new ArrayList<Class<?>>();
        findAnnotatedClasses(classList, new File("."), 0);
        writeConfigFile(fileName, classList.toArray(new Class[classList.size()]), sortClasses);
    }

    /**
     * writeConfigFile
     *
     * @param classes  Set to true to sort the classes by name before the file is generated.
     * @param fileName file name
     * @throws SQLException exception
     * @throws IOException  exception
     */
    public static void writeConfigFile(String fileName, Class<?>[] classes) throws SQLException, IOException {
        writeConfigFile(fileName, classes, false);
    }

    /**
     * writeConfigFile
     *
     * @param issortClasses Set to true to sort the classes by name before the file is generated.c
     * @param fileName      file name
     * @param classes       classes
     * @throws SQLException exception
     * @throws IOException  exception
     */
    public static void writeConfigFile(String fileName, Class<?>[] classes, boolean issortClasses)
            throws SQLException, IOException {
        File rawDir = findRawDir(new File("."));
        if (rawDir == null) {
            System.err.println("Could not find " + RAW_DIR_NAME + " directory which is typically in the "
                    + RESOURCE_DIR_NAME + " directory");
        } else {
            File configFile = new File(rawDir, fileName);
            writeConfigFile(configFile, classes, issortClasses);
        }
    }

    /**
     * writeConfigFile
     *
     * @param configFile config file
     * @throws SQLException exception
     * @throws IOException  exception
     */
    public static void writeConfigFile(File configFile) throws SQLException, IOException {
        writeConfigFile(configFile, false);
    }

    /**
     * writeConfigFile
     *
     * @param sortClasses Set to true to sort the classes by name before the file is generated.
     * @param configFile  config file
     * @throws SQLException exception
     * @throws IOException  exception
     */
    public static void writeConfigFile(File configFile, boolean sortClasses) throws SQLException, IOException {
        writeConfigFile(configFile, new File("."), sortClasses);
    }

    /**
     * Finds the annotated classes in the specified search directory or below and writes a configuration file.
     *
     * @param configFile config file
     * @param searchDir  file
     * @throws SQLException exception
     * @throws IOException  exception
     **/
    public static void writeConfigFile(File configFile, File searchDir) throws SQLException, IOException {
        writeConfigFile(configFile, searchDir, false);
    }

    /**
     * Finds the annotated classes in the specified search directory or below and writes a configuration file.
     *
     * @param sortClasses Set to true to sort the classes by name before the file is generated.
     * @param searchDir   file
     * @param configFile  config file
     * @throws SQLException exception
     * @throws IOException  exception
     */
    public static void writeConfigFile(File configFile, File searchDir, boolean sortClasses)
            throws SQLException, IOException {
        List<Class<?>> classList = new ArrayList<Class<?>>();
        findAnnotatedClasses(classList, searchDir, 0);
        writeConfigFile(configFile, classList.toArray(new Class[classList.size()]), sortClasses);
    }


    /**
     * Write a configuration file with the configuration for classes.
     *
     * @param configFile file name
     * @param classes    classes
     * @throws SQLException exception
     * @throws IOException  exception
     */
    public static void writeConfigFile(File configFile, Class<?>[] classes) throws SQLException, IOException {
        writeConfigFile(configFile, classes, false);
    }

    /**
     * Write a configuration file with the configuration for classes.
     *
     * @param sortClasses boolean
     * @param configFile  File
     * @param classes     Class
     * @throws SQLException Exception
     * @throws IOException  Exception
     */
    public static void writeConfigFile(File configFile, Class<?>[] classes, boolean sortClasses)
            throws SQLException, IOException {
        System.out.println("Writing configurations to " + configFile.getCanonicalPath());
        writeConfigFile(new FileOutputStream(configFile), classes, sortClasses);
    }

    /**
     * writeConfigFile
     *
     * @param searchDir    File
     * @param outputStream OutputStream
     * @throws SQLException Exception
     * @throws IOException  Exception
     */
    public static void writeConfigFile(OutputStream outputStream, File searchDir) throws SQLException, IOException {
        writeConfigFile(outputStream, searchDir, false);
    }

    /**
     * writeConfigFile.
     *
     * @param sortClasses  boolean
     * @param outputStream OutputStream
     * @param searchDir    SearchDir
     * @throws SQLException Exception
     * @throws IOException  Exception
     */
    public static void writeConfigFile(OutputStream outputStream, File searchDir, boolean sortClasses)
            throws SQLException, IOException {
        List<Class<?>> classList = new ArrayList<Class<?>>();
        findAnnotatedClasses(classList, searchDir, 0);
        writeConfigFile(outputStream, classList.toArray(new Class[classList.size()]), sortClasses);
    }

    /**
     * writeConfigFile
     *
     * @param outputStream OutputStream
     * @param classes      Classes
     * @throws SQLException Exception
     * @throws IOException  Exception
     */
    public static void writeConfigFile(OutputStream outputStream, Class<?>[] classes) throws SQLException, IOException {
        writeConfigFile(outputStream, classes, false);
    }

    /**
     * writeConfigFile
     *
     * @param classes      Classes
     * @param outputStream OutputStream
     * @param sortClasses  Set to true to sort the classes and fields by name before the file is generated.
     * @throws IOException  Exception
     * @throws SQLException Exception
     */
    public static void writeConfigFile(OutputStream outputStream, Class<?>[] classes, boolean sortClasses)
            throws SQLException, IOException {
        if (sortClasses) {
            Class<?>[] sortedClasses = new Class<?>[classes.length];
            System.arraycopy(classes, 0, sortedClasses, 0, classes.length);
            Arrays.sort(sortedClasses, CLASS_COMPARATOR);
            classes = sortedClasses;
        }
        BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(outputStream), Constants.MAGIC_4096);
        try {
            writeHeader(writer);
            for (Class<?> clazz : classes) {
                writeConfigForTable(writer, clazz, sortClasses);
            }
        } finally {
            writer.close();
        }
    }

    /**
     * Look for the resource-directory in the current directory or the directories above. Then look for the
     * raw-directory underneath the resource-directory.
     *
     * @param dir File
     * @return file
     */
    protected static File findRawDir(File dir) {
        for (int index = 0; dir != null && index < Constants.MAGIC_20; index++) {
            File rawDir = findResRawDir(dir);
            if (rawDir != null) {
                return rawDir;
            }
            dir = dir.getParentFile();
        }
        return null;
    }

    private static void writeHeader(BufferedWriter writer) throws IOException {
        writer.append('#');
        writer.newLine();
        writer.append("# generated on ").append(new SimpleDateFormat("yyyy/MM/dd hh:mm:ss").format(new Date()));
        writer.newLine();
        writer.append('#');
        writer.newLine();
    }

    private static void findAnnotatedClasses(List<Class<?>> classList, File dir, int level)
            throws SQLException, IOException {
        for (File file : dir.listFiles()) {
            if (file != null && file.isDirectory()) {
                if (level < MAXFINDSOURCELEVEL) {
                    findAnnotatedClasses(classList, file, level + 1);
                }
                continue;
            }
            if (!file.getName().endsWith(".java")) {
                continue;
            }
            String packageName = getPackageOfClass(file);
            if (packageName == null) {
                System.err.println("Could not find package name for: " + file);
                continue;
            }
            String name = file.getName();
            name = name.substring(0, name.length() - ".java".length());
            String className = packageName + "." + name;
            Class<?> clazz;
            try {
                clazz = Class.forName(className);
            } catch (Throwable t) {
                System.err.println("Could not load class file for: " + file);
                continue;
            }
            if (classHasAnnotations(clazz)) {
                classList.add(clazz);
            }
            try {
                for (Class<?> innerClazz : clazz.getDeclaredClasses()) {
                    if (classHasAnnotations(innerClazz)) {
                        classList.add(innerClazz);
                    }
                }
            } catch (Throwable t) {
                System.err.println("Could not load inner classes for: " + clazz);
                continue;
            }
        }
    }

    private static void writeConfigForTable(BufferedWriter writer, Class<?> clazz, boolean issortClasses)
            throws SQLException, IOException {
        String tableName = DatabaseTableConfig.extractTableName(DATABASETYPE, clazz);
        List<DatabaseFieldConfig> fieldConfigs = new ArrayList<DatabaseFieldConfig>();
        try {
            for (Class<?> working = clazz; working != null; working = working.getSuperclass()) {
                for (Field field : working.getDeclaredFields()) {
                    DatabaseFieldConfig fieldConfig = DatabaseFieldConfig.fromField(DATABASETYPE, tableName, field);
                    if (fieldConfig != null) {
                        fieldConfigs.add(fieldConfig);
                    }
                }
            }
        } catch (Error e) {
            System.err.println(
                    "Skipping " + clazz + " because we got an error finding its definition: " + e.getMessage());
            return;
        }
        if (fieldConfigs.isEmpty()) {
            System.out.println("Skipping " + clazz + " because no annotated fields found");
            return;
        }
        @SuppressWarnings({"rawtypes", "unchecked"})
        DatabaseTableConfig<?> tableConfig = new DatabaseTableConfig(clazz, tableName, fieldConfigs);
        DatabaseTableConfigLoader.write(writer, tableConfig);
        writer.append("#################################");
        writer.newLine();
    }

    private static boolean classHasAnnotations(Class<?> clazz) {
        while (clazz != null) {
            if (clazz.getAnnotation(DatabaseTable.class) != null) {
                return true;
            }
            Field[] fields;
            try {
                fields = clazz.getDeclaredFields();
            } catch (Throwable t) {
                System.err.println("Could not load get delcared fields from: " + clazz);
                System.err.println("     " + t);
                return false;
            }
            for (Field field : fields) {
                if (field.getAnnotation(DatabaseField.class) != null
                        || field.getAnnotation(ForeignCollectionField.class) != null) {
                    return true;
                }
            }
            try {
                clazz = clazz.getSuperclass();
            } catch (Throwable throwable) {
                System.err.println("Could not get super class for: " + clazz);
                System.err.println("     " + throwable);
                return false;
            }
        }

        return false;
    }

    /**
     * getPackageOfClass
     *
     * @param file File
     * @throws IOException
     * @return Package prefix string or null or no annotations.
     */
    private static String getPackageOfClass(File file) throws IOException {
        BufferedReader reader = new BufferedReader(new FileReader(file));
        try {
            while (true) {
                String line = reader.readLine();
                if (line == null) {
                    return null;
                }
                if (line.contains("package")) {
                    String[] parts = line.split("[ \t;]");
                    if (parts.length > 1 && parts[0].equals("package")) {
                        return parts[1];
                    }
                }
            }
        } finally {
            reader.close();
        }
    }

    /**
     * findResRawDir
     *
     * @param dir File
     * @return file
     */
    private static File findResRawDir(File dir) {
        for (File file : dir.listFiles()) {
            if (file.getName().equals(RESOURCE_DIR_NAME) && file.isDirectory()) {
                File[] rawFiles = file.listFiles(new FileFilter() {
                    @Override
                    public boolean accept(File file) {
                        return file.getName().equals(RAW_DIR_NAME) && file.isDirectory();
                    }
                });
                if (rawFiles.length == 1) {
                    return rawFiles[0];
                }
            }
        }
        return null;
    }

    /**
     * ClassComparator Compare fields by name.
     *
     */
    private static class ClassComparator implements Comparator<Class<?>> {
        @Override
        public int compare(Class<?> arg0, Class<?> arg1) {
            return arg0.getName().compareTo(arg1.getName());
        }
    }
}
